/**
 */

#include "mem_allocator.h"
#include <fcntl.h>
#include <sys/mman.h>
#include <algorithm>
#include <cstring>

#include <mutex>
#include "fake_log.h"

namespace MemTest {
//======================================
// MemMapMemAllocator
//======================================
std::mutex MemMapMemAllocator::mmap_mem_allocator_mutex_;

/**
 * When delegate setup direct memory mode,
 * this constructor will be called.
 * Do not panic in this constructor, just set
 * this instance to invalid if some error
 * occurs.
 */
static const int32_t MEM_FD_INIT = -1;

static const uint32_t MMAP_BASE_ADDR = 0x10000000;
static const uint32_t MMAP_HIGH_ADDR = 0x1DFFEFFF;

MemMapMemAllocator::MemMapMemAllocator(uint32_t in_base_addr, uint32_t in_high_addr)
    : mem_fd_(MEM_FD_INIT), map_base_addr_(nullptr), base_addr_(in_base_addr), high_addr_(in_high_addr) {
  if (base_addr_ == 0) {
    base_addr_ = MMAP_BASE_ADDR;
  }
  if (high_addr_ == 0) {
    high_addr_ = MMAP_HIGH_ADDR;
  }
  page_size_ = static_cast<uint32_t>(sysconf(_SC_PAGESIZE));
  MY_LOG_WARN("init with base_addr:%x, high_addr:%x", base_addr_, high_addr_);
  if (base_addr_ >= high_addr_) {
    MY_LOG_PANIC("base_addr >= high_addr, invalid input!");
  } else {
    mem_fd_ = open("/dev/mem", static_cast<uint32_t>(O_RDWR) | static_cast<uint32_t>(O_NDELAY));
    if (mem_fd_ < 0) {
      MY_LOG_WARN("open /dev/mem failed!");
    } else {
      const void *const ptr_void =
          mmap(nullptr, static_cast<size_t>(high_addr_) - base_addr_ + static_cast<uint32_t>(1),
               static_cast<uint32_t>(PROT_READ) | static_cast<uint32_t>(PROT_WRITE), static_cast<uint32_t>(MAP_SHARED),
               mem_fd_, base_addr_);
      if (ptr_void == MAP_FAILED) {
        MY_LOG_WARN("map failed!");
        const auto ret_value = close(mem_fd_);
        if (ret_value == -1) {
          MY_LOG_WARN("close failed!");
        }
        mem_fd_ = MEM_FD_INIT;
      } else {
        std::memcpy(&map_base_addr_, &ptr_void, sizeof(void *));
        range_list_ptr_ = std::make_shared<MemRange>(map_base_addr_, high_addr_ - base_addr_ + 1);
        range_list_ptr_->next_range = nullptr;
        range_list_ptr_->pre_range.reset();
        range_list_ptr_->is_dirty = false;

        MY_LOG_INFO("init MemMapMemAllocator success");
      }
    }
  }
}

MemMapMemAllocator::~MemMapMemAllocator() {
  range_list_ptr_ = nullptr;

  if (map_base_addr_ != MAP_FAILED) {
    const uint32_t tmp_addr = high_addr_ - base_addr_ + static_cast<uint32_t>(1);
    if (munmap(static_cast<void *>(map_base_addr_), static_cast<size_t>(tmp_addr)) == -1) {
      MY_LOG_WARN("munmap failed!");
    }
  }

  if (mem_fd_ >= 0) {
    if (close(mem_fd_) == -1) {
      MY_LOG_WARN("close failed!");
    }
  }
  mem_fd_ = MEM_FD_INIT;
}

int8_t *MemMapMemAllocator::mem_calloc(uint32_t block_num, uint32_t block_size) {
  const std::lock_guard<std::mutex> mem_lock(mmap_mem_allocator_mutex_);
  uint32_t size_need = block_num * block_size;

  int8_t *return_addr{nullptr};

  // align by page
  size_need += (page_size_ - 1);
  size_need &= ~(page_size_ - 1);
  if (size_need < page_size_) {
    size_need = page_size_;
  }

  // traversal range_ptr list to find the first applicable range_ptr
  const std::shared_ptr<MemRange> range_ptr = search_first_applicable_range_(size_need);
  if (range_ptr == nullptr) {
    MY_LOG_INFO("no range_ptr is applicable");
    return_addr = nullptr;
  } else {
    MY_LOG_DEBUG("[MemMapAllocator] will alloc %u bytes at %p", size_need, range_ptr->range_offset);
    return_addr = alloc_in_free_range_(range_ptr, size_need);

    MY_LOG_DEBUG("[MemMapAllocator] current range_ptr list:");
    std::shared_ptr<MemRange> current_range_ptr = range_list_ptr_;
    while (current_range_ptr) {
      MY_LOG_DEBUG("[MemMapAllocator] <start:%p, end:%p, block_size:%u, %s>", current_range_ptr->range_offset,
                   current_range_ptr->range_end, current_range_ptr->range_size,
                   current_range_ptr->is_dirty ? "dirty" : "free");
      current_range_ptr = current_range_ptr->next_range;
    }
  }

  return return_addr;
}

void MemMapMemAllocator::mem_free(const int8_t *const in_addr) {
  const std::lock_guard<std::mutex> mem_lock(mmap_mem_allocator_mutex_);
  // set a range_ptr's is_dirty flag to true.
  const std::shared_ptr<MemRange> range_ptr = find_target_range_(in_addr);
  if (range_ptr == nullptr) {
    MY_LOG_ERROR("try to free an invalid pointer %p", in_addr);
  } else {
    range_ptr->is_dirty = false;

    // merge ranges if the range_ptr before and/or after this range_ptr is free.
    if (const auto next_one = range_ptr->next_range) {
      if (!next_one->is_dirty) {
        (void)MemMapMemAllocator::merge_contiguous_range_(range_ptr, next_one);
      }
    }

    if (const auto pre_one = range_ptr->pre_range.lock()) {
      if (!pre_one->is_dirty) {
        (void)MemMapMemAllocator::merge_contiguous_range_(pre_one, range_ptr);
      }
    }
  }
}

bool MemMapMemAllocator::addr_is_valid(const int8_t *const in_addr) const {
  const uint32_t addr_diff = high_addr_ - base_addr_ + 1;
  return (in_addr >= map_base_addr_) && (in_addr < std::next(map_base_addr_, static_cast<std::ptrdiff_t>(addr_diff)));
}

bool MemMapMemAllocator::is_valid() const {
  return (mem_fd_ >= 0) && (map_base_addr_ != MAP_FAILED);
}

std::shared_ptr<MemRange> MemMapMemAllocator::search_first_applicable_range_(uint32_t size_need) const {
  std::shared_ptr<MemRange> range_ptr = range_list_ptr_;
  while (range_ptr != nullptr) {
    if ((range_ptr->is_dirty == true) || (range_ptr->range_size < size_need)) {
      range_ptr = range_ptr->next_range;
    } else {
      break;
    }
  }
  return range_ptr;
}

int8_t *MemMapMemAllocator::alloc_in_free_range_(const std::shared_ptr<MemRange> range_ptr, uint32_t size_need) {
  int8_t *return_addr{nullptr};
  if ((range_ptr == nullptr) || range_ptr->is_dirty || (size_need > range_ptr->range_size)) {
    MY_LOG_ERROR("Try to alloc memory in a inapplicable range_ptr!");
  } else {
    if (range_ptr->range_size - size_need < page_size_) {
      range_ptr->is_dirty = true;
      memset(range_ptr->range_offset, 0x0, range_ptr->range_size);
      return_addr = range_ptr->range_offset;
    } else {
      const auto alloc_range_ptr = std::make_shared<MemRange>(range_ptr->range_offset, size_need);
      alloc_range_ptr->is_dirty = true;
      alloc_range_ptr->next_range = range_ptr;
      alloc_range_ptr->pre_range = range_ptr->pre_range;
      if (const auto pre_one = range_ptr->pre_range.lock()) {
        pre_one->next_range = alloc_range_ptr;
      }
      // origin range_ptr is the first range_ptr, move head list forward
      if (range_list_ptr_ == range_ptr) {
        range_list_ptr_ = alloc_range_ptr;
      }
      range_ptr->range_offset = std::next(alloc_range_ptr->range_end, 1);
      range_ptr->range_size = range_ptr->range_size - size_need;
      range_ptr->pre_range = alloc_range_ptr;
      memset(alloc_range_ptr->range_offset, 0x0, alloc_range_ptr->range_size);
      return_addr = alloc_range_ptr->range_offset;
    }
  }
  return return_addr;
}

/**
 * Find the range_ptr which start at specific address
 * @param buffer
 * @return
 */
std::shared_ptr<MemRange> MemMapMemAllocator::find_target_range_(const int8_t *const in_addr) const {
  std::shared_ptr<MemRange> range_ptr = range_list_ptr_;
  while (range_ptr != nullptr) {
    if (range_ptr->range_offset != in_addr) {
      range_ptr = range_ptr->next_range;
    } else {
      break;
    }
  }
  return range_ptr;
}
/**
 * merge the second range_ptr to the first
 * @param first
 * @param second
 */
void MemMapMemAllocator::merge_contiguous_range_(const std::shared_ptr<MemRange> first_range_ptr,
                                                 const std::shared_ptr<MemRange> second_range_ptr) {
  if ((first_range_ptr == nullptr) || (second_range_ptr == nullptr) || (first_range_ptr->is_dirty) ||
      (second_range_ptr->is_dirty) || (std::next(first_range_ptr->range_end, 1) != second_range_ptr->range_offset)) {
    MY_LOG_ERROR("Merge range_ptr %p with %p is invalid", first_range_ptr.get(), second_range_ptr.get());
  } else {
    first_range_ptr->range_size = first_range_ptr->range_size + second_range_ptr->range_size;
    first_range_ptr->range_end = second_range_ptr->range_end;
    first_range_ptr->next_range = second_range_ptr->next_range;

    if (first_range_ptr->next_range) {
      first_range_ptr->next_range->pre_range = first_range_ptr;
    }
  }
}

//======================================
// DefaultMemAllocator
//======================================
int8_t *DefaultMemAllocator::allocate(uint32_t alloc_size) {
  int8_t *return_ptr{nullptr};
  // Find available blocks among pre-allocated memory blocks
  const auto it = std::find_if(free_blocks_.begin(), free_blocks_.end(), [&](const auto &block) {
    uint32_t block_size = static_cast<uint32_t>(block->size());
    bool ret_code = false;
    MY_LOG_INFO("block size: %u", block_size);
    if (block_size - used_sizes_[block] >= alloc_size) {
      ret_code = true;  // Found the block
    } else {
      // do nothing
    }
    return ret_code;
  });

  if (it != free_blocks_.end()) {
    return_ptr = std::next(const_cast<int8_t *>((*it)->data()), used_sizes_[*it]);
    used_sizes_[*it] += alloc_size;
    free_blocks_.erase(it);
  }

  if (return_ptr == nullptr) {
    MY_LOG_INFO("Not found available free block, will try new block!");
    uint32_t new_block_size{MEM_BLOCK_SIZE};
    if (alloc_size > new_block_size) {
      new_block_size = alloc_size;
    }
    // If no block is available, allocate a new block and insert it into the list
    std::vector<int8_t> mem_block(new_block_size);
    memory_blocks_.push_back(std::move(mem_block));
    used_sizes_[&memory_blocks_.back()] += new_block_size;
    return_ptr = memory_blocks_.back().data();
    if (return_ptr == nullptr) {
      MY_LOG_ERROR("%s", "Memory allocation failed.");
    }
  }
  return return_ptr;
}

void DefaultMemAllocator::deallocate(const int8_t *const in_ptr) {
  bool is_free_block{false};
  // Find the block of memory containing the pointer
  for (const auto &block : memory_blocks_) {
    const std::ptrdiff_t block_size = static_cast<std::ptrdiff_t>(block.size());
    if ((in_ptr >= block.data()) && (in_ptr < std::next(block.data(), block_size))) {
      // Find the corresponding memory block and mark it as available
      used_sizes_[&block] = static_cast<uint32_t>(block.data() - in_ptr);
      free_blocks_.push_back(&block);
      is_free_block = true;
    }
  }
  if (is_free_block == false) {
    // If the corresponding memory block cannot be found,
    // throw an exception or perform appropriate error handling
    MY_LOG_ERROR("%s", "Invalid pointer passed to deallocate.");
  }
}

//======================================
// MemAllocDelegate
//======================================
std::shared_ptr<MemAllocDelegate> MemAllocDelegate::alloc_dele_ptr_;
std::mutex MemAllocDelegate::delegate_mutex_;

std::shared_ptr<MemAllocDelegate> MemAllocDelegate::get_instance() {
  const std::lock_guard<std::mutex> local_lock(delegate_mutex_);
  if (!alloc_dele_ptr_) {
    alloc_dele_ptr_ = std::make_shared<MemAllocDelegate>();
    alloc_dele_ptr_->is_live_direct_memory_mode = false;
  }
  return alloc_dele_ptr_;
}

MemAllocDelegate::MemAllocDelegate() : allocation_cnt_(0) {
  default_allocator_ptr_ = std::make_shared<DefaultMemAllocator>(10);
  if (default_allocator_ptr_ != nullptr) {
    MY_LOG_INFO("init DefaultMemAllocator success");
  }
}

void MemAllocDelegate::setup_live_direct_memory_mode(uint32_t in_base_addr, uint32_t in_high_addr) {
  // add memmap_allocator into allocator list
  const std::unique_lock<std::mutex> lck(instance_mutex_);
  if (is_live_direct_memory_mode) {
    MY_LOG_INFO("Already in live direct memory mode.");
  } else {
    // Calling of this function means lidar is in direct memory mode.
    // Verify there is allocation before delegate setup direct memory mode.
    if (allocation_cnt_ != 0) {
      MY_LOG_WARN(
          "Did memory allocation with MemAllocDelegate before the direct"
          "memory mode setting up in direct memory mode!Please checking the usage of MemAllocDelegate");
    } else {
      MY_LOG_INFO("%p setup live direct memory mode with base_addr:%x, high_addr:%x", this, in_base_addr, in_high_addr);
      memmap_allocator_ptr_ = std::make_shared<MemMapMemAllocator>(in_base_addr, in_high_addr);
      if (memmap_allocator_ptr_ != nullptr && memmap_allocator_ptr_->is_valid()) {
        MY_LOG_INFO("init MemMapMemAllocator success.");
        is_live_direct_memory_mode = true;
      }
    }
  }
}

int8_t *MemAllocDelegate::mem_calloc(uint32_t in_block_num, uint32_t in_block_size, bool in_is_sys_malloc) {
  const std::lock_guard<std::mutex> lck(instance_mutex_);
  int8_t *alloc_rslt{nullptr};
  if (in_is_sys_malloc) {
    if (default_allocator_ptr_ != nullptr) {
      MY_LOG_INFO("%s calloc start", kDefaultMemAllocatorName);
      alloc_rslt = default_allocator_ptr_->mem_calloc(in_block_num, in_block_size);
    }
  } else {
    if (memmap_allocator_ptr_ != nullptr) {
      MY_LOG_INFO("%s calloc start", kMemMapMemAllocatorName);
      alloc_rslt = memmap_allocator_ptr_->mem_calloc(in_block_num, in_block_size);
    }
    if ((alloc_rslt == nullptr) && (default_allocator_ptr_ != nullptr)) {
      MY_LOG_INFO("%s calloc start", kDefaultMemAllocatorName);
      alloc_rslt = default_allocator_ptr_->mem_calloc(in_block_num, in_block_size);
    }
  }

  return alloc_rslt;
}

void MemAllocDelegate::mem_free(const int8_t *const in_addr) {
  const std::unique_lock<std::mutex> lck(instance_mutex_);

  bool free_rslt{false};
  if ((memmap_allocator_ptr_ != nullptr) && (memmap_allocator_ptr_->addr_is_valid(in_addr))) {
    memmap_allocator_ptr_->mem_free(in_addr);
    free_rslt = true;
  } else if (default_allocator_ptr_ != nullptr) {
    default_allocator_ptr_->mem_free(in_addr);
    free_rslt = true;
  } else {
    MY_LOG_ERROR("free %p failed", in_addr);
  }

  if (free_rslt == false) {
    MY_LOG_ERROR("free %p failed", in_addr);
  } else {
    MY_LOG_INFO("free %p success", in_addr);
  }
}

const char *MemAllocDelegate::get_allocator_name(const int8_t *const in_addr) const {
  const char *return_name{"unknown"};
  if ((memmap_allocator_ptr_ != nullptr) && (memmap_allocator_ptr_->addr_is_valid(in_addr))) {
    return_name = kMemMapMemAllocatorName;
  } else if (default_allocator_ptr_ != nullptr) {
    return_name = kDefaultMemAllocatorName;
  } else {
    MY_LOG_ERROR("get allocator name failed");
  }

  return return_name;
}
}  // namespace MemTest
